UNPKG

8.01 kBJavaScriptView Raw
1// Hacking too much time
2
3// Based on Node.js Module class sources:
4// https://github.com/nodejs/node/blob/master/lib/module.js
5
6import fs from 'fs'
7import path from 'path'
8import Module from 'module'
9
10import Log from './tools/log'
11import { ends_with } from './helpers'
12
13const original_findPath = Module._findPath
14
15export default class Require_hacker
16{
17 preceding_abstract_path_resolvers = []
18 abstract_path_resolvers = []
19
20 // original_loaders = {}
21
22 abstract_path_resolved_modules = {}
23
24 constructor(options)
25 {
26 // // take the passed in options
27 // this.options = clone(options)
28
29 // logging
30 this.log = new Log('require-hook', { debug: options.debug }) // this.options.debug
31
32 // instrument Module._findPath
33 // https://github.com/nodejs/node/blob/master/lib/module.js#L335-L341
34 Module._findPath = (...parameters) =>
35 {
36 const request = parameters[0]
37 // const paths = parameters[1]
38
39 // preceeding resolvers
40 for (let resolver of this.preceding_abstract_path_resolvers)
41 {
42 const resolved = resolver.resolve(request)
43 if (typeof resolved !== 'undefined')
44 {
45 return resolved
46 }
47 }
48
49 // original Node.js loader
50 const filename = original_findPath.apply(undefined, parameters)
51 if (filename !== false)
52 {
53 return filename
54 }
55
56 // rest resolvers
57 for (let resolver of this.abstract_path_resolvers)
58 {
59 const resolved = resolver.resolve(request)
60 if (typeof resolved !== 'undefined')
61 {
62 return resolved
63 }
64 }
65
66 return false
67 }
68 }
69
70 // installs a require() hook for paths
71 // which don't exist in the filesystem
72 //
73 // (if these paths exist in the filesystem
74 // then use the .hook(extension, resolve) method instead)
75 //
76 // id - a meaningful textual identifier
77 //
78 // resolver - a function which takes two parameters:
79 //
80 // the path to be resolved
81 //
82 // a function which flushes require() cache for this path
83 // with no parameters
84 //
85 // must return a javascript CommonJS module source code
86 // (i.e. "module.exports = ...", etc)
87 //
88 // returns an object with an .undo() method
89 //
90 resolver(id, resolver, options = {})
91 {
92 validate.resolver(id, resolver)
93
94 const resolver_entry =
95 {
96 id,
97 resolve: path =>
98 {
99 const resolved_path = `${path}.${id}`
100
101 // CommonJS module source code
102 const source = resolver(path)
103
104 if (typeof source === 'undefined')
105 {
106 return
107 }
108
109 // const flush_cache = () => delete require.cache[resolved_path]
110 delete require.cache[resolved_path]
111
112 this.abstract_path_resolved_modules[resolved_path] = source
113
114 return resolved_path
115 }
116 }
117
118 if (options.precede_node_loader)
119 {
120 this.preceding_abstract_path_resolvers.push(resolver_entry)
121 }
122 else
123 {
124 this.abstract_path_resolvers.push(resolver_entry)
125 }
126
127 const hook = this.hook(id, path =>
128 {
129 const source = this.abstract_path_resolved_modules[path]
130 delete this.abstract_path_resolved_modules[path]
131 return source
132 })
133
134 const result =
135 {
136 unmount: () =>
137 {
138 // javascript arrays still have no .remove() method in the XXI-st century
139 this.preceding_abstract_path_resolvers = this.preceding_abstract_path_resolvers.filter(x => x !== resolver_entry)
140 this.abstract_path_resolvers = this.abstract_path_resolvers.filter(x => x !== resolver_entry)
141 hook.unmount()
142 }
143 }
144
145 return result
146 }
147
148 // installs a require() hook for the extension
149 //
150 // extension - a file extension to hook into require()s of
151 // (examples: 'css', 'jpg', 'js')
152 //
153 // resolve - a function that takes two parameters:
154 //
155 // the path requested in the require() call
156 //
157 // and a fallback function (fall back to default behaviour)
158 // with no parameters
159 //
160 // must return a javascript CommonJS module source code
161 // (i.e. "module.exports = ...", etc)
162 //
163 hook(extension, resolve)
164 {
165 this.log.debug(`Hooking into *.${extension} files loading`)
166
167 // validation
168 validate.extension(extension)
169 validate.resolve(resolve)
170
171 // dotted extension
172 const dot_extension = `.${extension}`
173
174 // keep original extension loader
175 const original_loader = Module._extensions[dot_extension]
176
177 // display a warning in case of extension loader override
178 if (original_loader)
179 {
180 const output = (extension === 'js' ? this.log.debug : this.log.warning).bind(this.log)
181 output(`-----------------------------------------------`)
182 output(`Overriding an already existing require() hook `)
183 output(`for file extension ${dot_extension}`)
184 output(`-----------------------------------------------`)
185 }
186
187 // the list of cached modules
188 const cached_modules = new Set()
189
190 // set new loader for this extension
191 Module._extensions[dot_extension] = (module, filename) =>
192 {
193 this.log.debug(`Loading source code for ${filename}`)
194
195 // fallback flag
196 let aborted = false
197
198 // var source = fs.readFileSync(filename, 'utf8')
199 const source = resolve(filename, () =>
200 {
201 this.log.debug(`Fallback to original loader`)
202
203 // fallen back
204 aborted = true
205
206 // this message would appear if there was no loader
207 // for the extension of the filename
208 if (path.extname(filename) !== dot_extension)
209 {
210 this.log.info(`Trying to load "${path.basename(filename)}" as a "*${dot_extension}"`)
211 }
212
213 // load the file with the original loader
214 (original_loader || Module._extensions['.js'])(module, filename)
215 })
216
217 // if fallen back - exit
218 if (aborted)
219 {
220 return
221 }
222
223 // add this file path to the list of cached modules
224 cached_modules.add(filename)
225
226 // compile javascript module from its source
227 // https://github.com/nodejs/node/blob/master/lib/module.js#L379
228 module._compile(source, filename)
229 }
230
231 const result =
232 {
233 // uninstall the hook
234 unmount: () =>
235 {
236 // clear require() cache for this file extension
237 for (let path of cached_modules)
238 {
239 delete require.cache[path]
240 }
241
242 // mount the original loader for this file extension
243 Module._extensions[dot_extension] = original_loader
244 }
245 }
246
247 return result
248 }
249
250 // // uninstalls a previously installed require() hook for the extension
251 // //
252 // // extension - the file extension for which to uninstall
253 // // the previously installed require() hook
254 // //
255 // unhook(extension)
256 // {
257 // this.log.debug(`Unhooking from .${extension} files loading`)
258 //
259 // // validation
260 // validate.extension(extension)
261 //
262 // // dotted extension
263 // const dot_extension = `.${extension}`
264 //
265 // // verify that the hook exists in the first place
266 // if (Object.keys(this.original_loaders).indexOf(dot_extension) < 0)
267 // {
268 // throw new Error(`Require() hook wasn't previously installed for ${dot_extension} files`)
269 // }
270 //
271 // // uninstall the hook
272 // Module._extensions[dot_extension] = this.original_loaders[dot_extension]
273 // delete this.original_loaders[dot_extension]
274 // }
275}
276
277// validation
278const validate =
279{
280 extension(extension)
281 {
282 if (typeof extension !== 'string')
283 {
284 throw new Error(`Expected string extension. Got ${extension}`)
285 }
286
287 if (path.extname(`test.${extension}`) !== `.${extension}`)
288 {
289 throw new Error(`Invalid file extension ${extension}`)
290 }
291 },
292
293 resolve(resolve)
294 {
295 if (typeof resolve !== 'function')
296 {
297 throw new Error(`Resolve should be a function. Got ${resolve}`)
298 }
299 },
300
301 resolver(id, resolver)
302 {
303 if (!id)
304 {
305 throw new Error(`You must specify resolver id`)
306 }
307
308 if (path.extname(`test.${id}`) !== `.${id}`)
309 {
310 throw new Error(`Invalid resolver id. Expected a valid file extension.`)
311 }
312 }
313}
\No newline at end of file